package uz.rt.dicom.io;

import uz.rt.dicom.ValueRepresentation;
import uz.rt.dicom.data.ElementFactory;
import uz.rt.dicom.data.IDataElement;
import uz.rt.dicom.data.element.SequenceItem;
import uz.rt.dicom.dictionary.IDicomDictionary;
import uz.rt.dicom.tag.DicomTag;
import uz.rt.dicom.tag.Tag;
import uz.rt.dicom.uid.TransferSyntax;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.logging.Logger;

/**
 * The dicom input stream is an extention of the BinaryInputStream and adds a transfer syntax that defines how the
 * dicom
 * data is read.
 */
public class DicomInputStream extends BinaryInputStream
{
// ------------------------------ FIELDS ------------------------------

    /** Dicom Input Stream logger */
    private final static Logger logger = Logger.getLogger(DicomInputStream.class.getName());
    /** Dicom dictionary */
    private IDicomDictionary dictionary = null;
    /** Transfer syntax defining the format of the dicom that is being read */
    private TransferSyntax transferSyntax;

// --------------------------- CONSTRUCTORS ---------------------------

    /**
     * Creates a dicom input stream and initialises the transfer syntax to its default value.
     *
     * @param in Input stream
     */
    protected DicomInputStream(InputStream in)
    {
        super(in);
        transferSyntax = TransferSyntax.Default;
    }

    /**
     * Creates a dicom input stream and defines the transfer syntax.
     *
     * @param in             Input stream
     * @param transferSyntax Transfer syntax defining the format of the dicom that is read
     */
    protected DicomInputStream(InputStream in, TransferSyntax transferSyntax)
    {
        super(in, transferSyntax.getByteOrder());
        this.transferSyntax = transferSyntax;
    }

    /**
     * Creates a dicom input stream and defines the byte order.
     *
     * @param in        Input stream
     * @param byteOrder Byte order which is used to read the dicom input stream
     */
    protected DicomInputStream(InputStream in, ByteOrder byteOrder)
    {
        super(in, byteOrder);
        transferSyntax = TransferSyntax.Default;
    }

// --------------------- GETTER / SETTER METHODS ---------------------

    /**
     * Get a dicom dictionary
     *
     * @return Dicom dictionary
     */
    public IDicomDictionary getDictionary()
    {
        return dictionary;
    }

    /**
     * Set a dicom dictionary
     *
     * @param dictionary Dicom dictionary
     */
    public void setDictionary(IDicomDictionary dictionary)
    {
        this.dictionary = dictionary;
    }

    /**
     * Get the dicom transfer syntax
     *
     * @return Dicom transfer syntax
     */
    public TransferSyntax getTransferSyntax()
    {
        return transferSyntax;
    }

// -------------------------- OTHER METHODS --------------------------

    public synchronized int getPosition()
    {
        return super.position;
    }

    /**
     * Read a dicom element from the Dicom input stream. The reading process begins by reading a dicom tag and
     * determining the value representation depending on the implicit or explicit status. Afterwards the number of
     * bytes
     * for the dicom element value is read. Eventually the data is read into the IDataElement element.
     *
     * @return IDataElement dicom element
     * @throws IOException If an IO error occurs.
     */
    public synchronized IDataElement readElement() throws IOException
    {
        DicomTag tag = this.readDicomTag();
        ValueRepresentation vr;
        int length;
        if (isExplicitVR())
        {
            vr = new ValueRepresentation(readValueRepresentationBytes());
            if (vr.equals(ValueRepresentation.OtherByteString) || vr.equals(ValueRepresentation.OtherWordString) ||
                    vr.equals(ValueRepresentation.OtherFloatString) || vr.equals(ValueRepresentation.SequenceOfItems) ||
                    vr.equals(ValueRepresentation.UnlimitedText) || vr.equals(ValueRepresentation.Unknown))
            {
                if (this.skip(2) != 2)
                    throw new DicomIOException(
                            "Number of bytes skipped in the dicom input stream differs from the expected number of bytes.");
                long tmp = readUnsigned32();
                if (tmp > Integer.MAX_VALUE)
                    throw new DicomIOException("Can not handle lengths that are bigger then " + Integer.MAX_VALUE);
                length = (int) tmp;
            } else
                length = readUnsigned16();
        } else
        {
            vr = dictionary.getTagVR(tag);
            long tmp = readUnsigned32();
            if (tmp > Integer.MAX_VALUE)
                throw new DicomIOException("Can not handle lengths that are bigger then " + Integer.MAX_VALUE);
            length = (int) tmp;
        }
        IDataElement element;
        if (tag == Tag.ItemTag)
        {
            vr = ValueRepresentation.NotApplicable;
            element = new SequenceItem(tag);
            element.setValueRepresentation(vr);
        } else
        {
            element = ElementFactory.getInstance().getElement(tag, vr);
            element.setValueRepresentation(vr);
        }
        element.read(this, length);
        return element;
    }

    /**
     * Read a dicom tag from a stream
     *
     * @return Dicom tag
     * @throws IOException If an IO error occurs.
     */
    public synchronized DicomTag readDicomTag() throws IOException
    {
        DicomTag tag = new DicomTag(0x0000, 0x0000);
        byte[] bytes = new byte[2];
        if (readToBuffer(bytes, 0, 2) != 2)
            throw new IOException("Unable to read 2 bytes from stream to a buffer.");
        tag.setGroup(bytes, getByteOrder());
        if (readToBuffer(bytes, 0, 2) != 2)
            throw new IOException("Unable to read 2 bytes from stream to a buffer.");
        tag.setElement(bytes, getByteOrder());
        return tag;
    }

    /**
     * Is the value representation explicit?
     *
     * @return True if value representation is explicit, if implicit false.
     */
    boolean isExplicitVR()
    {
        return transferSyntax.isExplicitVR();
    }

    /**
     * Read the value representation in bytes.
     *
     * @return An array of bytes (lenght = 2)
     * @throws DicomIOException If the transfer syntax is set to implicit
     * @throws IOException      If an IO error occurs.
     */
    synchronized byte[] readValueRepresentationBytes() throws IOException
    {
        if (!isExplicitVR()) throw new DicomIOException(
                "Transfer syntax uses implict, therefore the value representation can not be read");
        byte[] bytes = new byte[2];
        if (readToBuffer(bytes, 0, 2) != 2)
            throw new IOException("Unable to read 2 bytes from stream to a buffer.");
        return bytes;
    }

    /**
     * Read a dicom element from the Dicom input stream. The reading process begins by reading a dicom tag and
     * determining the value representation depending on the implicit or explicit status. Afterwards the number of
     * bytes
     * for the dicom element value is read. Eventually the data is read into the IDataElement element.
     *
     * @param tag Dicom tag
     * @return IDataElement dicom element
     * @throws IOException If an IO error occurs.
     */
    public synchronized IDataElement readElement(DicomTag tag) throws IOException
    {
        ValueRepresentation vr;
        int length;
        if (isExplicitVR())
        {
            vr = new ValueRepresentation(readValueRepresentationBytes());
            if (vr.equals(ValueRepresentation.OtherByteString) || vr.equals(ValueRepresentation.OtherWordString) ||
                    vr.equals(ValueRepresentation.OtherFloatString) || vr.equals(ValueRepresentation.SequenceOfItems) ||
                    vr.equals(ValueRepresentation.UnlimitedText) || vr.equals(ValueRepresentation.Unknown))
            {
                if (this.skip(2) != 2)
                    throw new DicomIOException(
                            "Number of bytes skipped in the dicom input stream differs from the expected number of bytes.");
                long tmp = readUnsigned32();
                if (tmp > Integer.MAX_VALUE)
                    throw new DicomIOException("Can not handle lengths that are bigger then " + Integer.MAX_VALUE);
                length = (int) tmp;
            } else
                length = readUnsigned16();
        } else
        {
            vr = dictionary.getTagVR(tag);
            long tmp = readUnsigned32();
            if (tmp > Integer.MAX_VALUE)
                throw new DicomIOException("Can not handle lengths that are bigger then " + Integer.MAX_VALUE);
            length = (int) tmp;
        }
        IDataElement element;
        if (tag == Tag.ItemTag)
        {
            vr = ValueRepresentation.NotApplicable;
            element = new SequenceItem(tag);
            element.setValueRepresentation(vr);
        } else
        {
            element = ElementFactory.getInstance().getElement(tag, vr);
            element.setValueRepresentation(vr);
        }
        element.read(this, length);
        return element;
    }

    /**
     * Set the dicom transfer syntax
     *
     * @param transferSyntax Dicom transfer syntax
     */
    public void setTransferSyntax(TransferSyntax transferSyntax)
    {
        this.transferSyntax = transferSyntax;
        this.setByteOrder(this.transferSyntax.getByteOrder());
    }
}
